package cn.ii8080.i8.gateway.server.config;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.List;

/**
 * token校验
 */
@Configuration
public class TokenAuthenticationConverter implements ServerAuthenticationConverter {
    /**
     * redis连接工厂
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Autowired
    private TokenStore tokenStore;

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        String value = exchange.getRequest().getPath().pathWithinApplication().value();

        String accessToken = extractHeaderToken(exchange);
        if (StringUtils.isEmpty(accessToken)) {
            if (value.startsWith("/auth/oauth")) {
                return Mono.just(new AnonymousAuthenticationToken("anonymous", "anonymous", AuthorityUtils.commaSeparatedStringToAuthorityList("anonymous")));
            }
        }
        OAuth2AccessToken token = tokenStore.readAccessToken(accessToken);
        if (token == null) {
            if (value.startsWith("/auth/oauth/")) {
                return Mono.just(new AnonymousAuthenticationToken("anonymous", "anonymous", AuthorityUtils.commaSeparatedStringToAuthorityList("anonymous")));
            } else {
                throw new InvalidTokenException("Token was not recognised");
            }

        }
        if (token.isExpired()) {
            if (value.startsWith("/auth/oauth/")) {
                return Mono.just(new AnonymousAuthenticationToken("anonymous", "anonymous", AuthorityUtils.commaSeparatedStringToAuthorityList("anonymous")));
            } else {
                throw new InvalidTokenException("Token has expired");
            }
        }
        OAuth2Authentication authentication = tokenStore.readAuthentication(accessToken);
        if (authentication == null) {
            if (value.startsWith("/auth/oauth/")) {
                return Mono.just(new AnonymousAuthenticationToken("anonymous", "anonymous", AuthorityUtils.commaSeparatedStringToAuthorityList("anonymous")));
            } else {
                throw new InvalidTokenException("Invalid access token: " + accessToken);
            }

        }
        if (value.startsWith("/auth/oauth/")) {
            return Mono.just(authentication);
        }else if (value.startsWith("/auth/user/")) {
            return Mono.just(authentication);
        }else if (value.startsWith("/auth/logout")) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.OK);
            return Mono.just(new AnonymousAuthenticationToken("anonymous", "anonymous", AuthorityUtils.commaSeparatedStringToAuthorityList("anonymous")));
        }
        byte[] bytes = null;
        try {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            ObjectOutputStream oo = new ObjectOutputStream(bo);
            oo.writeObject(authentication);
            bytes = bo.toByteArray();
            bo.close();
            oo.close();
        } catch (Exception e) {
        }
        try {
            String tokenBase64 = Base64.getEncoder().encodeToString(bytes);
            ServerHttpRequest host = exchange.getRequest().mutate().header("Authorization", OAuth2AccessToken.BEARER_TYPE + " " + tokenBase64).build();
            exchange.mutate().request(host).build();
        } catch (Exception e) {
        }
        return Mono.just(authentication);
    }

    public static String extractHeaderToken(ServerWebExchange exchange) {
        List<String> headers = exchange.getRequest().getHeaders().get("Authorization");
        if (headers == null || headers.size() == 0) {
            return null;
        }
        for (String value : headers) { // typically there is only one (most servers enforce that)
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // Add this here for the auth details later. Would be better to change the signature of this method.
                exchange.getAttributes().put(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
                        value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }
        return null;
    }
}
